/**
 * jSpecial project bootstrap file.
 * http://code.google.com/p/jspecial/
 *
 * @author Andrey Anisimov <hercules@inbox.ru>
 */

/**
 * Binds the specified function to the specified object as a method.
 *
 * @param obj (object) The object to bind method to.
 * @param method (function) The function to bind.
 */
function bind(obj, method) {
    return function () {
        return method.apply(obj, arguments);
    }
}

/**
 * Returns classpath for the class loader.
 * Note: this function defines a classpath automatically by
 * script src attribute. Custom classpath will be implemented later.
 *
 * @return string
 */
function classpath() {
    var scripts = document.getElementsByTagName('script');
    for (var i = 0, n = scripts.length; i < n; ++i) {
        var src = scripts.item(i).src;
        if (src && src.match(/^(.*?)\/loader\.js(.*)$/)) {
            var defaultPath = RegExp.$1;
            var queryString = RegExp.$2;
            if (queryString.match(/[\?&]classpath=(.*?)$/)) {
                return RegExp.$1.split('|');
            }
            return [defaultPath];
        }
    }
    return '';
}

/**
 * Cross-browser HTTP request factory.
 */
var XMLHttpRequestFactory = {

    /**
     * Creates and returns cross-browser request object.
     * @return XMLHttpRequest
     */
    createRequest:function () {
        if (window.XMLHttpRequest) {
            return new XMLHttpRequest();
        }
        if (window.ActiveXObject) {
            try {
                return new ActiveXObject('Msxml2.XMLHTTP');
            } catch (e) {
            }
            try {
                return new ActiveXObject('Microsoft.XMLHTTP');
            } catch (e) {
            }
        }
        return null;
    }
};

/**
 * Loads files synchronously via HTTP. Please note that if you want
 * to load files asyncronously, you probably need to use core.ajax.sendRequest
 * function instead of this one.
 */
var HTTPFileLoader = {

    /**
     * Loads XML file and returns the result Document object.
     *
     * @param path (string) File path to load.
     * @return DOMDocument
     * @throws Error When the specified file is invalid XML or when it was not found.
     */
    loadXml:function (path) {
        var request = this.requestFile(path);
        if (!request.responseXML) {
            throw new Error(path + ' is not a valid text/xml file.');
        }
        return request.responseXML.documentElement;
    },

    /**
     * Loads text file and returns its content.
     *
     * @param path (string) File path to load.
     * @return string
     * @throws Error When the specified file was not found.
     */
    loadFile:function (path) {
        return this.requestFile(path).responseText;
    },

    /**
     * Sends request to server to load the specified file.
     *
     * @param path (string) File path to load.
     * @return XMLHttpRequest
     */
    requestFile:function (path) {
        try {
            var request = XMLHttpRequestFactory.createRequest();
            request.open('GET', path, false);
            request.send(null);
            if ((200 === request.status) || (304 === request.status)) {
                return request;
            }
        } catch (e) {
        }
        throw new Error('Could not find file: ' + path);
    }
};

function Class(pkg, className) {
    this.name = pkg.name + '.' + className;
    this.simpleName = className;

    // "Class" is just a function in JavaScript, so, this function should
    // a) append all class-specific methods and properties to the new object
    // b) append all superclass methods and properties, and
    // c) call the init function (a constructor in Java terminology).
    //
    // Since we can't extend Function or any of the predefined
    // objects, we need the result to be a function.

    eval("this.func = function " + className + "(proto) {\n\
            if (proto != '___proto___') { // No need to call constructor for prototyping\n\
                var clazz = this.getClass();\n\
                if (!clazz.initialized) {\n\
                    clazz.initialize();\n\
                }\n\
                this[className].apply(this, arguments);\n\
            }\n\
        }");

    this.func.class = this;
    this.func.toString = new Function('return \'[Class ' + this.name + ']\'');
    return this.func;
}

/**
 * @private
 */
Class.prototype.makePrototype = function () {
    if (this.prototypeCreated) {
        return;
    }
    var proto;
    if (this.super) {
        proto = new this.super.func('___proto___');
    } else {
        proto = {};
    }
    var clazz = this;
    proto.getClass = function () {
        return clazz;
    };
    proto.toString = function () {
        return '[Instance of ' + clazz.getName() + ']';
    };
    this.func.prototype = proto;
    this.prototypeCreated = true;
};

/**
 * @type {Boolean}
 * Whether the class has been initialized.
 */
Class.prototype.initialized = false;

/**
 * @private
 * Apply all class members to the prototype.
 */
Class.prototype.initialize = function () {
    if (this.initialized) {
        return;
    }
    if (this.super) {
        this.super.initialize();
    }
    this.applyMembers(this.func.prototype);
    this.initialized = true;
};

Class.prototype.applyMembers = function (instance) {
    var obj = {};
    this.body.apply(obj);
    for (var key in obj) {
        if (('function' == typeof instance[key]) && this.super) {
            // Override super class method.
            instance['___' + this.super.simpleName + '_' + key] = instance[key];
        }
        instance[key] = obj[key];
    }
    // Ensure that we have all constructors for all superclasses.
    // This will allow us to use this.BaseClass even if we don't know
    // whether it was defined in the base class or not.
    if (!instance[this.simpleName]) {
        if (this.super) {
            var _super = instance[this.super.simpleName];
            instance[this.simpleName] = function () {
                _super.apply(this, arguments);
            };
        } else {
            instance[this.simpleName] = function () {};
        }
    }
};

Class.prototype.getName = function() {
    return this.name;
};

Class.prototype.getSimpleName = function() {
    return this.simpleName;
};

// Returns true if this class is one of subclasses for the specified base class.
Class.prototype.isSubclassOf = function (baseClass) {
    var clazz = this;
    while (clazz.super) {
        clazz = clazz.super;
        if (baseClass === clazz) return true;
    }
    return false;
};

/**
 * Replaces all reserved constructions with standard JavaScript.
 *
 * @private
 * @param script (string) Class definition script.
 */
Class.prototype.processScript = function (script) {
    var className = this.simpleName;
    script = script.replace(/import\s+(.*\.(.*?))\s*;/g, 'var $2 = load(\'$1\');');
    script = script.replace(/class\s+\S+\s+extends\s+(\S+)\s*\{/, className + '.class.super = $1.class; ' + className + '.class.body = function() {');
    var superClassName = RegExp.$1;
    if (superClassName.indexOf('.') > 0) {
        superClassName = superClassName.replace(/(.*\.(.*?))/, '$2');
    }
    script = script.replace(/class\s+\S+\s*\{/, className + '.class.body = function() {');
    if (superClassName) {
        script = script.replace(/([^\w\.])super.(\w+)(\s*?)\(/g, '$1this[\'___' + superClassName + '_$2\']$3(');
        script = script.replace(/([^\w\.])super(\s*?)\(/g, '$1this[\'' + superClassName + '\']$2(');
    }
    try {
        eval('(function(' + className + '){' + script + "})(this.func)");
    } catch (e) {
        throw new Error('Error in class ' + this.name + ': [' + e.name + '] ' + e.message);
    }
};

/**
 * @public
 * @param name {String}
 * @return {Class}
 */
Class.forName = function(name) {
    return DefaultClassLoader.load(name);
};

/**
 * @public
 * @param {Object} pkg
 * @param {String} className
 * @param {Function} classFunc
 * @return {Class}
 */
Class.create = function (pkg, className, classFunc) {
    if (!pkg[className]) {
        var newClass = new Class(pkg, className);
        pkg[className] = newClass;
        classFunc(newClass);
    }
    return pkg[className];
};

/**
 * @public
 * @param {Object} pkg
 * @param {String} className
 * @param {String} script
 * @return {Class}
 */
Class.createFromScript = function (pkg, className, script) {
    if (!pkg[className]) {
        var clazz = new Class(pkg, className);
        pkg[className] = clazz;
        clazz.class.processScript(script);
        clazz.class.makePrototype();
    }
    return pkg[className];
};

/**
 * Class loader object. Not to be used directly.
 */
function ClassLoader(classpath) {

    /**
     * Returns the class by its fully-qualified name.
     *
     * @public
     * @param fqClassName (string) Fully-qualified class name.
     * @return Class
     */
    this.getClassByName = function (fqClassName) {
        return this.load(fqClassName);
    };

    /**
     * @public
     * Loads class by its fully-qualified name. Wildcard (*)
     * supported to load all classes in package.
     *
     * @param name (string) Fully-qualified class name.
     */
    this.load = function (name) {
        var pkg = null;
        var i = name.lastIndexOf('.');
        if (i > 0) {
            pkg = this.createPackage(name.substr(0, i));
        }
        return loadClass(pkg, name.substr(i + 1));
    };

    /**
     * @public
     * Creates package object and all its subpackages. If load argument is true,
     * then this method will attempt to load package descriptor file from server.
     *
     * @param packageName (string) Fully qualified name of the package.
     * @throws Error when package descriptor file cannot be found.
     */
    this.createPackage = function(packageName) {
        try {
            var existing = eval(packageName);
            if (existing) {
                return existing;
            }
        } catch (e) {
        }
        var owner = window;
        var subpkgs = packageName.split('.');
        var current = subpkgs.pop();
        if (subpkgs.length > 0) {
            owner = this.createPackage(subpkgs.join('.'));
        }
        var pkg = {
            name:packageName,
            classes:{},
            toString:function () {
                return '[Package ' + this.name + ']';
            }
        };
        return owner[current] = pkg;
    };

    /**
     * @private
     * Loads class into existing package.
     *
     * @param pkg (Package) Package object (can be <code>null</code> in case of no package).
     * @param className (string) Name of the class to load.
     */
    function loadClass(pkg, className) {
        var fqClassName = (pkg) ? (pkg.name + '.' + className) : className;
        try {
            var existing = eval(fqClassName);
            if (existing) {
                return existing;
            }
        } catch (e) {
        }
        var script;
        for (var i = 0, n = classpath.length; i < n; ++i) {
            try {
                var filePath = classpath[i] + '/' + fqClassName.replace(/\./g, '/') + '.js';
                script = HTTPFileLoader.loadFile(filePath);
                break;
            } catch (e) {
            }
        }
        if (script) {
            return Class.createFromScript(pkg, className, script);
        }
        throw new Error('Class ' + fqClassName + ' not found in classpath ' + classpath.join('|'));
    }
}

var DefaultClassLoader = new ClassLoader(classpath());

/**
 * Loads class by its name. Supports wildcard (*) to load all classes in package.
 *
 * @param name (string) Fully-qualified class name or package name plus *.
 * @throws Error if load fails.
 */
function load(name) {
    return DefaultClassLoader.load(name);
}